#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "messaggio.h"

//Variabili globali contenenti il riferimento al thread di lettura e a quello di scrittura
pthread_t reader_tid, writer_tid;

//Variabili globali usati per gestire il socket per comunicare con il server
struct sockaddr_in client;
int descr;

//Variabile globale go per il controllo dei vari cicli dei thread
int go = 1;

//Stringhe in cui salvare gli input all'avvio del client
char username[MAX_CHAR], name[MAX_CHAR], email[MAX_CHAR];

//Prototipo delle funzioni
int main(int, char**);
void stampaGuida(void);
void connetti(void);
void *threadWriter(void*);
void *threadReader(void*);
void logout(void);
int contaOccorrenze(char*, char);
void gestoreSegnali(int);


/*
 * Procedura principale che viene eseguita dal client
 *
 * @param Intero contenente il numero delle opzioni al momento dell'avvio
 * @param Puntatore a vettore di caratteri contenente tutte le opzioni di avvio
 */
int main(int argc, char *argv[]) {

	//Preparo il client per la gestione dei segnali
	signal(SIGINT, gestoreSegnali);
	signal(SIGTERM, gestoreSegnali);

	//Controllo della prima opzione per capire l'azione da eseguire
	if(argc - 1 == 0) {

		//Esecuzione errata del client
		fprintf(stderr, "Errore, nessuna opzione inserita\n");

		//Stampo la guida
		stampaGuida();

	//Controllo se è stato inserito il parametro -h
	} else if (strcmp(argv[1], "-h") == 0) {

		//Stampo la guida
		stampaGuida();

	//Controllo se è stato inserito il parametro -r
	} else if ((strcmp(argv[1], "-r") == 0) && (argc - 1 == 5)) {

		//Copio i parametri inseriti
		strcpy(name, argv[2]);
		strcat(name, " ");
		strcat(name, argv[3]);
		strcpy(email, argv[4]);
		strcpy(username, argv[5]);

		//Controllo se sono inseriti simboli che non dovrebbero esserci
		if((contaOccorrenze(username, ':') + contaOccorrenze(username, '#') >= 1) ||
				(contaOccorrenze(name, ':') + contaOccorrenze(name, '#') >= 1) ||
				(contaOccorrenze(email, ':') + contaOccorrenze(email, '#') >= 1) ||
				(contaOccorrenze(email, '@') != 1)) {

			//Esecuzione errata del client
			fprintf(stderr, "Errore, opzioni errate inserite\n");

			//Stampo la guida
			stampaGuida();

		//Se i parametri inseriti sono corretti procedo con i tentativi di connessione
		} else

			//Eseguo la connessione
			connetti();

	} else if (argc - 1 == 1) {

		//Presente un solo parametro, quindi devo eseguire un login con l'username = parametro 1
		strcpy(username, argv[1]);

		//Controllo se sono inseriti simboli che non dovrebbero esserci
		if(contaOccorrenze(username, ':') + contaOccorrenze(username, '#') >= 1) {

			//Esecuzione errata del client
			fprintf(stderr, "Errore, opzioni errate inserite\n");

			//Stampo la guida
			stampaGuida();

		//Se i parametri inseriti sono corretti procedo con i tentativi di connessione
		} else

			//Eseguo la connessione
			connetti();

	} else {

		//Esecuzione errata del client
		fprintf(stderr, "Errore, opzioni errate inserite\n");

		//Stampo la guida
		stampaGuida();
	}

	//Termino l'esecuzione
	exit(0);
}


/*
 * Procedura che tenta di eseguire la connessione con i server, creando i thread
 * se la connessione è avvenuta con successo
 */
void connetti() {

	//Variabili utili per la connessione al server
	int tentativi = 0, connesso = -1, size = 0;
	char * temp;
	messaggio * msx = malloc(sizeof(messaggio));

	//Creo il socket e ne salvo il descrittore in descr
	descr = socket(AF_INET, SOCK_STREAM, 0);

	//Controllo che il socket sia stato creato senza problemi
	if(descr < 0) {
		fprintf(stderr, "Errore durante l'apertura del socket, terminazione del client...\n");
		exit(-1);
	}

	//Azzero la struttura contenente i parametri del socket
	bzero((char*) &client, sizeof(struct sockaddr_in));

	// inizializzazione dell'indirizzo del server
	client.sin_family = AF_INET;
	client.sin_port = htons(12345);
	client.sin_addr.s_addr = inet_addr("127.0.0.1");

	/* ****************************************************************** *
	 * Tentativi di connessione al server                                 *
	 * ****************************************************************** */

	//Procedo con la connessione al server
	do {

		//Tento la connessione
		connesso = connect(descr, (struct sockaddr*)&client, sizeof(client));

		//Controllo se la connessione è avvenuta con successo
		if(connesso < 0) {

			//Scrivo che non riesco a collegarmi e attendo un secondo prima di ritentare
			fprintf(stderr, "Impossibile connettersi, tentativo di riconnessione %d\n", ++tentativi);
			sleep(1);
		}
	} while((connesso < 0) && (tentativi < 10));

	//Controllo se la connessione è avvenuta
	if(connesso < 0) {

		//Stampo messaggio di errore e fermo l'esecuzione
		fprintf(stderr, "Impossibile connettersi, terminazione del client...\n");
		exit(-1);

	/* ****************************************************************** *
	 * Invio messaggio di login o register and login                      *
	 * ****************************************************************** */

	//Se la connessione è avvenuta con successo devo inviare il messaggio
	} else {

		//Controllo se effettuare un login o una registrazione e login
		if(strlen(name) == 0) {

			//Preparo il messaggio per l'invio
			resettaMessaggio(msx);
			msx -> type = MSG_LOGIN;
			msx -> msglen = strlen(username);
			msx -> msg = malloc(sizeof(char) * msx -> msglen);
			strcpy(msx -> msg, username);
			temp = malloc(sizeof(char) * grandezzaMessaggio(msx));
			impacchettaMessaggio(msx, temp);
			size = strlen(temp);

			//Invio il messaggio
			if((write(descr, &size, sizeof(int)) < 0) || (write(descr, temp, strlen(temp) + 1) < 0)) {

				//Se viene restituito errore, allora il socket è chiuso
				fprintf(stderr, "Errore durante il login, terminazione del client...\n");
				exit(-1);
			}

		//Eseguo quindi una registrazione con login
		} else {

			//Preparo il messaggio per l'invio
			resettaMessaggio(msx);
			msx -> type = REG_LOGIN;
			msx -> msglen = strlen(username) + 1 + strlen(name) + 1 + strlen(email);
			msx -> msg = malloc(sizeof(char) * msx -> msglen);
			strcpy(msx -> msg, username);
			strcat(msx -> msg, ":");
			strcat(msx -> msg, name);
			strcat(msx -> msg, ":");
			strcat(msx -> msg, email);
			temp = malloc(grandezzaMessaggio(msx));
			impacchettaMessaggio(msx, temp);
			size = strlen(temp);

			//Invio il messaggio
			if((write(descr, &size, sizeof(int)) < 0) || (write(descr, temp, strlen(temp) + 1) < 0)) {

				//Se viene restituito errore, allora il socket è chiuso
				fprintf(stderr, "Errore durante la registrazione, terminazione del client...\n");
				exit(-1);
			}
		}

		/* ****************************************************************** *
		 * Attendo risposta dal server                                        *
		 * ****************************************************************** */

		//Attendo la risposta dal server
		do {

			//Resetto il messaggio per preparare la struttura ad essere scritta
			resettaMessaggio(msx);

			//Controllo se è possibile leggere dal socket
			if(read(descr, &size, sizeof(int)) <= 0) {

				//Se viene restituito errore, allora il socket è chiuso
				fprintf(stderr, "Errore lettura durante il login, terminazione del client...\n");
				exit(-1);
			}

			//Dealloco temp per poter scegliere la dimensione della memoria per la lettura
			free(temp);

			//Alloco una quantità necessaria per contenere il messaggio
			temp = malloc(sizeof(char) * size);

			//Controllo se è possibile leggere dal socket
			if(read(descr, temp, size + 1) <= 0){

				//Se viene restituito errore, allora il socket è chiuso
				fprintf(stderr, "Errore lettura messaggio durante il login, terminazione del client...\n");
				exit(-1);
			}

			//Depacchetto il messaggio ricevuto
			depacchettaMessaggio(msx, temp);

		} while(msx -> type != MSG_OK && msx -> type != MSG_ERROR);

		/* ****************************************************************** *
		 * Controllo la risposta e avvio o no i thread                        *
		 * ****************************************************************** */

		//Se la risposta del server è un errore, stampo il messaggio di errore
		if(msx -> type == MSG_ERROR){

			//Se viene restituito errore, allora il socket è chiuso
			fprintf(stderr, "%s\n", msx -> msg);
			exit(-1);
		}

		//Avvio il thread per la scrittura
		if(pthread_create(&writer_tid, NULL, threadWriter, NULL) < 0) {
			fprintf(stderr, "Errore durante la creazione del thread writer, terminazione del client...\n");
			exit(-1);
		}

		//Avvio il thread per la lettura
		if(pthread_create(&reader_tid, NULL, threadReader, NULL) < 0) {
			fprintf(stderr, "Errore durante la creazione del thread reader, terminazione del client...\n");
			exit(-1);
		}

		//Attendo la terminazione dei thread
		pthread_join(writer_tid, NULL);
		pthread_join(reader_tid, NULL);
		printf("Chiusura del client...\n");
	}
}


/*
 * Procedura eseguita dal thread writer come da specifiche
 *
 * @param Puntatore a void non utilizzato
 */
void *threadWriter(void *argv) {

	//Variabili per la gestione dei messaggi
	messaggio * msx = malloc(sizeof(messaggio));
	char input[MAX_CHAR * 8], receiver[MAX_CHAR], text[MAX_CHAR * 7];
	char command[MAX_CHAR], * tempinput, * temp = malloc(sizeof(char));
	int i, size, result = 0;

	//Sopprime warning durante la compilazione
	result = result;

	//Messaggio utile per indicare l'attesa di input
	printf("\nIn attesa di input...\n");

	//Eseguo il ciclo della variabile globale go
	while(go) {

		/* ****************************************************************** *
		 * Ottengo e controllo input                                          *
		 * ****************************************************************** */

		//Ottengo l'input da tastiera
		result = scanf(" %[^\n]", input);

		//Controllo che non ci siano simboli che non devono esserci
		if((contaOccorrenze(input, '#') != 1) && (input[0] != '#')) {

			//Stampo l'errore a video
			fprintf(stderr, "Errore, inserire un comando come indicato dalla guida\n");
			stampaGuida();

		//Se l'input è un comando, allora controllo cosa è stato inserito
		} else {

			//Separo il comando dai campi inseriti
			tempinput = strtok(input, " ");
			if(tempinput == NULL)
				strcpy(command, " ");
			else
				strcpy(command, tempinput);

			//Messaggio singolo
			if(input[6] != ':') {

				//Ottengo il destinatario
				tempinput = strtok(NULL, ":");
				if(tempinput == NULL)
					strcpy(receiver, "");
				else
					strcpy(receiver, tempinput);

				//Ottengo il messaggio
				tempinput = strtok(NULL, "\0");
				if(tempinput == NULL)
					strcpy(text, " ");
				else
					strcpy(text, tempinput);

			//Altrimenti non c'è destinatario
			} else {

				//Ottengo il messaggio
				strcpy(receiver, "");
				tempinput = strtok(NULL, "\0");
				if(tempinput == NULL)
					strcpy(text, " ");
				else
					strcpy(text, tempinput);

				//Elimino ":" iniziali dovuti al broadcast
				for(i = 0; i < strlen(text); i++)
					text[i] = text[i + 1];
			}

			//Libero la memoria occupata da temp per preparare alla nuova scrittura
			free(temp);

			/* ****************************************************************** *
			 * Controllo il comando (caso list)                                 *
			 * ****************************************************************** */

			//Controllo il comando per capire quale messaggio inviare (caso list)
			if(strcmp(command, "#ls") == 0) {

				//Preparo il messaggio per l'invio
				resettaMessaggio(msx);
				msx -> type = (char) MSG_LIST;
				temp = malloc(grandezzaMessaggio(msx));
				impacchettaMessaggio(msx, temp);
				size = strlen(temp);

				//Invio il messaggio
				if((write(descr, &size, sizeof(int)) < 0) || (write(descr, temp, strlen(temp) + 1) < 0)) {

					//Se viene restituito errore, allora il socket è chiuso
					go = 0;
					close(descr);
					pthread_kill(reader_tid, SIGKILL);
					pthread_exit(NULL);
				}

			/* ****************************************************************** *
			 * Controllo il comando (caso logout)                                 *
			 * ****************************************************************** */

			//Caso logout
			} else if(strcmp(command, "#logout") == 0) {

				//Invio il logout
				logout();

			/* ****************************************************************** *
			 * Controllo il comando (caso dest, messaggio singolo)                *
			 * ****************************************************************** */

			//Caso dest
			} else if((strcmp(command, "#dest") == 0) && strlen(text) > 0) {

				//Controllo se è presente il destinatario
				if(strlen(receiver) <= 1) {

					//Preparo il messaggio per l'invio
					resettaMessaggio(msx);
					msx -> type = (char) MSG_BRDCAST;
					msx -> msglen = strlen(text);
					msx -> msg = malloc(sizeof(char) * msx -> msglen);
					strcpy(msx -> msg, text);
					temp = malloc(grandezzaMessaggio(msx));
					impacchettaMessaggio(msx, temp);
					size = strlen(temp);

					//Invio il messaggio
					if((write(descr, &size, sizeof(int)) < 0) || (write(descr, temp, strlen(temp) + 1) < 0)) {

						//Se viene restituito errore, allora il socket è chiuso
						go = 0;
						close(descr);
						pthread_kill(reader_tid, SIGKILL);
						pthread_exit(NULL);
					}

				/* ****************************************************************** *
				 * Controllo tipo di messaggio per l'invio (caso broadcast)           *
				 * ****************************************************************** */

				//Caso di invio a destinatario singolo
				} else {

					//Preparo il messaggio per l'invio
					resettaMessaggio(msx);
					msx -> type = (char) MSG_SINGLE;
					msx -> receiver = malloc(sizeof(char) * strlen(receiver));
					strcpy(msx -> receiver, receiver);
					msx -> msglen = strlen(text);
					msx -> msg = malloc(sizeof(char) * msx -> msglen);
					strcpy(msx -> msg, text);
					temp = malloc(grandezzaMessaggio(msx));
					impacchettaMessaggio(msx, temp);
					size = strlen(temp);

					//Invio il messaggio
					if((write(descr, &size, sizeof(int)) <= 0) || (write(descr, temp, strlen(temp) + 1) <= 0)) {

						//Se viene restituito errore, allora il socket è chiuso
						go = 0;
						close(descr);
						pthread_kill(reader_tid, SIGKILL);
						pthread_exit(NULL);
					}
				}

			} else {

				//Stampo messaggio di errore poichè il comando non è riconosciuto
				fprintf(stderr, "Errore, inserito un comando non riconosciuto\n");
				stampaGuida();
			}
		}
	}

	//Termino l'esecuzione del thread writer
	go = 0;
	close(descr);
	pthread_exit(NULL);
}


/*
 * Procedura eseguita dal thread reader come da specifiche
 *
 * @param Puntatore a void non utilizzato
 */
void *threadReader(void *argv) {

	//Variabili per la gestione dei messaggi
	int size = 0;
	char * temp = malloc(sizeof(char));
	messaggio * msx = malloc(sizeof(messaggio));

	//Eseguo il ciclo della variabile globale go
	while(go) {

		//Leggo dal socket la lunghezza del messaggio successivo
		if(read(descr, &size, sizeof(int)) <= 0) {

			//Se viene restituito errore, allora il socket è chiuso
			go = 0;
			fprintf(stderr, "Connessione con il server chiusa\n");
			close(descr);
			pthread_kill(writer_tid, SIGKILL);
			pthread_exit(NULL);
		}

		//Dealloco temp per poter scegliere la dimensione della memoria per la lettura
		free(temp);

		//Alloco una zona capace di contenere il messaggio e poi leggo dal socket
		temp = malloc(sizeof(char) * size);

		//Leggo il messaggio dal socket
		if(read(descr, temp, size + 1) <= 0) {

			//Se viene restituito errore, allora il socket è chiuso
			go = 0;
			fprintf(stderr, "Connessione con il server chiusa\n");
			close(descr);
			pthread_kill(writer_tid, SIGKILL);
			pthread_exit(NULL);
		}

		//Resetto la struttura che contiene il messaggio
		resettaMessaggio(msx);

		//Depacchetto il messaggio ricevuto
		depacchettaMessaggio(msx, temp);

		/* ****************************************************************** *
		 * Controllo il tipo di messaggio per capire cosa stampare a video    *
		 * ****************************************************************** */

		//Determino le operazioni da eseguire in base al tipo di messaggio
		switch(msx -> type) {

		//Caso di risposta ok
		case MSG_OK:

			//Stampo che l'operazione precedente è andata a buon fine
			printf("Operazione completata\n");
			break;

		//Caso di risposta errore
		case MSG_ERROR:

			//Stampo il messaggio di errore
			fprintf(stderr, "%s\n", msx -> msg);
			break;

		//Caso di risposta a list
		case MSG_LIST:

			//Stampo il messaggio
			printf("%s\n", msx -> msg);
			break;

		//Caso di messaggio singolo
		case MSG_SINGLE:

			//Stampo il messaggio
			printf("%s:%s:%s\n", msx -> sender, username, msx -> msg);
			break;

		//Caso di messaggio broadcast
		case MSG_BRDCAST:

			//Stampo il messaggio
			printf("%s:*:%s\n", msx -> sender, msx -> msg);
			break;
		}

		//Messaggio utile per indicare l'attesa di input
		printf("\nIn attesa di input...\n");

	}

	//Termino l'esecuzione del thread reader
	go = 0;
	close(descr);
	pthread_exit(NULL);
}


/*
 * Procedura che stampa a video una guida sul funzionamento dell'eseguibile
 */
void stampaGuida() {

	//Stampo una guida
	printf("	-----Guida per l'utilizzo del client di chat-----\n\n");
	printf("Esecuzione tipica:\n");
	printf("	 ./chat-client [-h] [-r Name Surname Email] username\n");
	printf("L'username indica l'identificativo dell'utente ed è obbligatorio\n");
	printf("Vietato l'utilizzo di : o #\n");
	printf("Opzioni:\n\n");
	printf("-h 	 Visualizza la guida del funzionamento dell'eseguibile\n");
	printf("-r 	 Esegue una connessione al server con registrazione\n");
	printf("   	 utilizzando le stringhe immesse tra \"\" come informazioni\n");
	printf("   	 dell'utente. Name, Surname ed Email sono obbligatorie\n");
	printf("   	 utilizzando questa opzione\n\n");
	printf("Comandi:\n\n");
	printf("#dest destinatario:testo\n");
	printf("   	 destinatario specifica a chi è destinato il messaggio.\n");
	printf("   	 testo è il messaggio che verrà effettivamente inviato.\n");
	printf("   	 Se il destinatario non viene inserito, il messaggio\n");
	printf("   	 verrà inviato in broadcast, quindi a tutti gli utenti\n");
	printf("   	 è necessario usare \":\" prima del testo\n");
	printf("   	 Vietato # all'interno del messaggio\n");
	printf("#ls	 Visualizza la lista degli utenti attualmente online\n");
	printf("#logout	 Effettua la disconnessione dal server e termina \n");
	printf("   	 l'eseguibile\n");
	printf("#dest	 Invia un messaggio ad un destinatario \n");
}


/*
 * Procedura che esegue il logout dal server
 */
void logout() {

	//Intero per salvare il risultato delle scritture (Evito warning)
	int result = 0;

	//Sopprime warning durante la compilazione
	result = result;

	//Variabili utili per l'invio del messaggio
	messaggio * msx = malloc(sizeof(messaggio));
	char * temp;
	int size;

	//Preparo il messaggio per l'invio
	resettaMessaggio(msx);
	msx -> type = (char) MSG_LOGOUT;
	temp = malloc(grandezzaMessaggio(msx));
	impacchettaMessaggio(msx, temp);
	size = strlen(temp);

	//Invio il messaggio
	result = write(descr, &size, sizeof(int));
	result = write(descr, temp, strlen(temp) + 1);

	//Se viene restituito errore, allora il socket è chiuso
	go = 0;
	close(descr);
	pthread_exit(NULL);
}


/*
 * Funzione per il controllo del numero di ripetizione di un carattere
 * all'interno di una stringa
 *
 * @param Stringa in cui controllare quante volte compare il carattere
 * @param Carattere da cercare all'interno di string
 * @return Intero, numero di occorrenze di symbol in Stringa
 */
int contaOccorrenze(char string[], char symbol) {

	//Variabili utili per il conto delle occorrenze
	int i, occorrenze = 0;

	//Scorro tutta la stringa
	for(i = 0; i < strlen(string); i++) {

		//Aggiorno il numero di occorrenze
		if(string[i] == symbol)
			occorrenze++;
	}

	//Return del numero di occorrenze
	return occorrenze;
}


/*
 * Procedura per la gestione dei segnali di terminazione
 *
 * @param Intero che contiene il segnale inserito da console
 */
void gestoreSegnali(int sig) {

	//Controllo il segnale in input
	if(sig == SIGTERM || sig == SIGINT) {

		//Eseguo procedura di arresto del server impostando go a 0
		go = 0;

		//Eseguo logout
		logout();

		//Chiudo il reader
		pthread_kill(reader_tid, SIGKILL);
	}
}
